﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Xml;
using System.IO;
using System.Data.Common;
using ICSharpCode.SharpZipLib.Zip;
using ICSharpCode.SharpZipLib.Checksums;
using System.Text;
using System.Threading;

/// <summary>
/// Allows users to backup or restore the database ASynchronously
/// </summary>
public partial class Backup : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        lblRestoreError.Visible = false;
    }
    protected void btnBackup_Click(object sender, EventArgs e) {
        if(!chkBakApplications.Checked && !chkBakElements.Checked && !chkBakResources.Checked && !chkBakUsers.Checked && !chkBakTemplates.Checked) {
            lblNoBackupOptions.Visible = true;
            return;
        }
        XmlDataDocument doc = new XmlDataDocument();
        XmlDeclaration decl = (XmlDeclaration)doc.CreateNode(XmlNodeType.XmlDeclaration, "", "");
        decl.Encoding = "UTF-8";
        doc.AppendChild(decl);
        XmlNode root = doc.CreateElement("resourceblender");
        doc.AppendChild(root);

        XmlElement ndMeta = doc.CreateElement("meta");
        root.AppendChild(ndMeta);
        XmlElement ndGenerator = doc.CreateElement("generator");
        ndGenerator.InnerText = "ResourceBlender "+Constants.Version.ToString(3);
        ndMeta.AppendChild(ndGenerator);
        XmlElement ndGenerationDate = doc.CreateElement("generated");
        ndGenerationDate.InnerText = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss G\\MT");
        ndMeta.AppendChild(ndGenerationDate);

        Database db = new Database();
        db.OpenConnection();
        if(chkBakApplications.Checked) {
            using(Database.Command cmd = db.CreateCommand()) {
                XmlElement ndApplications = doc.CreateElement("applications");
                root.AppendChild(ndApplications);
                cmd.CommandText = "SELECT * FROM " + db.TablePrefix + "applications";
                DbDataReader reader = cmd.ExecuteReader();
                while(reader.Read()) {
                    XmlElement eleApplication = doc.CreateElement("application");
                    ndApplications.AppendChild(eleApplication);

                    XmlElement eleId = doc.CreateElement("id");
                    eleId.InnerText = HttpUtility.HtmlEncode(reader["id"].ToString());
                    eleApplication.AppendChild(eleId);

                    XmlElement eleName = doc.CreateElement("name");
                    eleName.InnerText = Utils.HtmlEncode(reader["name"].ToString());
                    eleApplication.AppendChild(eleName);

                    XmlElement eleDisplayname = doc.CreateElement("displayname");
                    eleDisplayname.InnerText = Utils.HtmlEncode(reader["displayname"].ToString());
                    eleApplication.AppendChild(eleDisplayname);

                    XmlElement eleDefaultLanguage = doc.CreateElement("defaultlanguage");
                    eleDefaultLanguage.InnerText = HttpUtility.HtmlEncode(reader["defaultlanguage"].ToString());
                    eleApplication.AppendChild(eleDefaultLanguage);

                    XmlElement eleLanguages = doc.CreateElement("languages");
                    eleLanguages.InnerText = HttpUtility.HtmlEncode(reader["languages"].ToString());
                    eleApplication.AppendChild(eleLanguages);
                }
                reader.Close();
                XmlElement ndBundles = doc.CreateElement("bundles");
                root.AppendChild(ndBundles);
                cmd.CommandText = "SELECT * FROM " + db.TablePrefix + "bundles";
                reader = cmd.ExecuteReader();
                while(reader.Read()) {
                    XmlElement eleBundle = doc.CreateElement("bundle");
                    ndBundles.AppendChild(eleBundle);

                    XmlElement eleId = doc.CreateElement("id");
                    eleId.InnerText = HttpUtility.HtmlEncode(reader["id"].ToString());
                    eleBundle.AppendChild(eleId);

                    XmlElement eleName = doc.CreateElement("name");
                    eleName.InnerText = Utils.HtmlEncode(reader["name"].ToString());
                    eleBundle.AppendChild(eleName);

                    XmlElement eleApplicationId = doc.CreateElement("application");
                    eleApplicationId.InnerText = Utils.HtmlEncode(reader["application"].ToString());
                    eleBundle.AppendChild(eleApplicationId);
                }
                reader.Close();
            }
        }
        if(chkBakElements.Checked) {
            using(Database.Command cmd = db.CreateCommand()) {
                XmlElement ndElements = doc.CreateElement("elements");
                root.AppendChild(ndElements);
                cmd.CommandText = "SELECT * FROM " + db.TablePrefix + "elements";
                DbDataReader reader = cmd.ExecuteReader();
                while(reader.Read()) {
                    XmlElement eleElement = doc.CreateElement("element");
                    ndElements.AppendChild(eleElement);

                    XmlElement eleApplicationId = doc.CreateElement("applicationid");
                    eleApplicationId.InnerText = HttpUtility.HtmlEncode(reader["applicationid"].ToString());
                    eleElement.AppendChild(eleApplicationId);

                    XmlElement eleBundleName = doc.CreateElement("bundle");
                    eleBundleName.InnerText = Utils.HtmlEncode(reader["bundle"].ToString());
                    eleElement.AppendChild(eleBundleName);

                    XmlElement eleElementName = doc.CreateElement("elementname");
                    eleElementName.InnerText = Utils.HtmlEncode(reader["elementname"].ToString());
                    eleElement.AppendChild(eleElementName);

                    XmlElement eleResourceId = doc.CreateElement("resourceid");
                    eleResourceId.InnerText = reader["resourceid"].ToString();
                    eleElement.AppendChild(eleResourceId);

                    XmlElement eleComment = doc.CreateElement("comment");
                    eleComment.InnerText = Utils.HtmlEncode(reader["comment"].ToString());
                    eleElement.AppendChild(eleComment);
                }
                reader.Close();
            }
        }
        if(chkBakResources.Checked) {
            using(Database.Command cmd = db.CreateCommand()) {
                XmlElement ndResources = doc.CreateElement("resources");
                root.AppendChild(ndResources);
                cmd.CommandText = "SELECT r.resourceid AS resourceid, language, translation, translatorcomment, comment FROM " + db.TablePrefix + "resources AS r LEFT JOIN " + db.TablePrefix + "resourcecomments AS c ON r.resourceid = c.resourceid ORDER BY r.resourceid ASC";
                DbDataReader reader = cmd.ExecuteReader();
                string lastResourceId = string.Empty,
                       currentResourceId = string.Empty;
                XmlElement eleResource = null;
                while(reader.Read()) {
                    currentResourceId = reader["resourceid"].ToString();
                    if(currentResourceId != lastResourceId) {
                        eleResource = doc.CreateElement("resource");
                        ndResources.AppendChild(eleResource);

                        XmlElement eleId = doc.CreateElement("id");
                        eleId.InnerText = HttpUtility.HtmlEncode(currentResourceId);
                        eleResource.AppendChild(eleId);

                        XmlElement eleComment = doc.CreateElement("comment");
                        eleComment.InnerText = Utils.HtmlEncode(reader["comment"].ToString());
                        eleResource.AppendChild(eleComment);

                        XmlElement eleLanguages = doc.CreateElement("languages");
                        eleResource.AppendChild(eleLanguages);
                    }
                    lastResourceId = currentResourceId;
                    XmlElement eleTranslatedLanguage = doc.CreateElement("language");
                    eleTranslatedLanguage.InnerText = Utils.HtmlEncode(reader["translation"].ToString());
                    eleResource.LastChild.AppendChild(eleTranslatedLanguage);

                    XmlAttribute attrAbbreviation = doc.CreateAttribute("code");
                    attrAbbreviation.InnerText = reader["language"].ToString();
                    eleTranslatedLanguage.Attributes.Append(attrAbbreviation);

                    string translatorComment = reader["translatorcomment"].ToString();
                    if(translatorComment.Length > 0) {
                        XmlAttribute attrTranslatorComment = doc.CreateAttribute("translatorcomment");
                        attrTranslatorComment.InnerText = Utils.HtmlEncode(translatorComment);
                        eleTranslatedLanguage.Attributes.Append(attrTranslatorComment);
                    }
                }
                reader.Close();
            }
        }
        if(chkBakTemplates.Checked) {
            using(Database.Command cmd = db.CreateCommand()) {
                XmlElement ndElements = doc.CreateElement("templates");
                root.AppendChild(ndElements);
                cmd.CommandText = "SELECT * FROM " + db.TablePrefix + "templates";
                DbDataReader reader = cmd.ExecuteReader();
                while(reader.Read()) {
                    XmlElement eleElement = doc.CreateElement("template");
                    ndElements.AppendChild(eleElement);

                    XmlElement eleTemplateName = doc.CreateElement("name");
                    eleTemplateName.InnerText = Utils.HtmlEncode(reader["name"].ToString());
                    eleElement.AppendChild(eleTemplateName);

                    XmlElement eleFileName = doc.CreateElement("filename");
                    eleFileName.InnerText = Utils.HtmlEncode(reader["filename"].ToString());
                    eleElement.AppendChild(eleFileName);

                    XmlElement eleFileStart = doc.CreateElement("filestart");
                    eleFileStart.InnerText = Utils.HtmlEncode(reader["filestart"].ToString());
                    eleElement.AppendChild(eleFileStart);

                    XmlElement eleFileEnd = doc.CreateElement("fileend");
                    eleFileEnd.InnerText = Utils.HtmlEncode(reader["fileend"].ToString());
                    eleElement.AppendChild(eleFileEnd);

                    XmlElement eleLine = doc.CreateElement("line");
                    eleLine.InnerText = Utils.HtmlEncode(reader["line"].ToString());
                    eleElement.AppendChild(eleLine);
                }
                reader.Close();
            }
        }
        if(chkBakUsers.Checked) {
            using(Database.Command cmd = db.CreateCommand()) {
                XmlElement ndUsers = doc.CreateElement("users");
                root.AppendChild(ndUsers);
                cmd.CommandText = "SELECT * FROM " + db.TablePrefix + "users";
                DbDataReader reader = cmd.ExecuteReader();
                while(reader.Read()) {
                    XmlElement eleUser = doc.CreateElement("user");
                    ndUsers.AppendChild(eleUser);

                    XmlElement eleId = doc.CreateElement("id");
                    eleId.InnerText = HttpUtility.HtmlEncode(reader["id"].ToString());
                    eleUser.AppendChild(eleId);

                    XmlElement eleUsername = doc.CreateElement("username");
                    eleUsername.InnerText = Utils.HtmlEncode(reader["username"].ToString());
                    eleUser.AppendChild(eleUsername);

                    XmlElement elePassword = doc.CreateElement("password");
                    elePassword.InnerText = Utils.HtmlEncode(reader["password"].ToString());
                    eleUser.AppendChild(elePassword);

                    XmlElement eleEmail = doc.CreateElement("email");
                    eleEmail.InnerText = Utils.HtmlEncode(reader["email"].ToString());
                    eleUser.AppendChild(eleEmail);

                    XmlElement elePermissions = doc.CreateElement("permissions");
                    elePermissions.InnerText = HttpUtility.HtmlEncode(reader["permissions"].ToString());
                    eleUser.AppendChild(elePermissions);

                    XmlElement eleAllowedProjects = doc.CreateElement("allowedprojects");
                    eleAllowedProjects.InnerText = Utils.HtmlEncode(reader["allowedprojects"].ToString());
                    eleUser.AppendChild(eleAllowedProjects);
                }
                reader.Close();
            }
        }

        using(MemoryStream msDoc = new MemoryStream())
        using(MemoryStream msZip = new MemoryStream()) {
            string fileName = "ResourceBlender" + DateTime.Now.ToString("yyyy-MM-dd-HHmm");

            ZipOutputStream zipStream = new ZipOutputStream(msZip);
            zipStream.SetLevel(9);

            doc.Save(msDoc);
            byte[] buffer = msDoc.ToArray();
            msDoc.Close();
            ZipEntry zipEntry = new ZipEntry(fileName + ".xml");
            zipEntry.DateTime = DateTime.Now;
            zipEntry.Size = buffer.Length;
            zipStream.PutNextEntry(zipEntry);
            zipStream.Write(buffer, 0, buffer.Length);
            zipStream.Finish();

            Response.Clear();
            Response.AddHeader("Content-Type", "binary/octet-stream");
            Response.AddHeader("Content-Length", zipStream.Length.ToString());
            Response.AddHeader("Content-Disposition", "attachment; filename=" + fileName + ".zip; size=" + zipStream.Length.ToString());
            Response.Flush();

            Response.BinaryWrite(msZip.ToArray());
            msZip.Close();
            Response.Flush();
            Response.End();
        }
    }

    /// <summary>
    /// Detects the type (zip/xml) of uploaded file and parses it to an XmlDataDocument
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
    protected void btnRestore_Click(object sender, EventArgs e) {
        if(!uplBackupfile.HasFile) {
            lblNoFileSelected.Visible = true;
            return;
        } else if(!chkResApplications.Checked && !chkResElements.Checked && !chkResResources.Checked && !chkResUsers.Checked && !chkResTemplates.Checked) {
            lblRestoreError.Text = "Nothing selected to restore";
            lblRestoreError.Visible = true;
            return;
        }
        try {
            XmlDataDocument doc = new XmlDataDocument();
            string ext = Path.GetExtension(uplBackupfile.FileName).ToLowerInvariant();
            switch(ext) {
                case ".xml":
                    doc.Load(uplBackupfile.FileContent);
                    break;
                case ".zip":
                    using(MemoryStream ms = new MemoryStream()) {
                        ZipInputStream zipStream = new ZipInputStream(uplBackupfile.FileContent);
                        ZipEntry entry = zipStream.GetNextEntry();
                        byte[] buffer = new byte[1024];
                        int size;
                        while((size = zipStream.Read(buffer, 0, buffer.Length)) > 0) {
                            ms.Write(buffer, 0, size);
                        }
                        ms.Seek(0, SeekOrigin.Begin);
                        doc.Load(ms);
                        Session["restoreFile"] = doc;
                    }
                    break;
                default:
                    throw new ArgumentException("Unsupported file type - "+ext);
            }
            lstProgress.Items.Add("Uploaded backup file, click 'Restore' to start.");
            mltMain.ActiveViewIndex = 1;
        } catch(Exception ex) {
            lblRestoreError.Text = "Error restoring #406: " + ex.Message;
            lblRestoreError.Visible = true;
        }
    }

    /// <summary>
    /// Starts a background thread and enables a timer to show the status of the restore operation.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
    protected void btnStartRestore_Click(object sender, EventArgs e) {
        XmlDataDocument doc = Session["restoreFile"] as XmlDataDocument;
        if(doc == null) {
            lblRestoreError.Text = "Error #408: Could not load file from cache.";
            lblRestoreError.Visible = true;
            return;
        }
        lstProgress.Items.Clear();
        Thread worker = new Thread(new ThreadStart(StartRestore));
        imgProgress.Visible = true;
        worker.Name = "RestoreWorker";
        worker.IsBackground = true;
        worker.Start();
        Session["worker"] = worker;
        List<ListItem> log = new List<ListItem>();
        log.Add(new ListItem("Starting restore...")); // the log is deserialized on every timer tick
        Session["log"] = log;
        Session["restoreDone"] = bool.FalseString;
        tmrRestore.Enabled = true;
        btnStartRestore.Enabled = false;
    }

    /// <summary>
    /// Starts the restoration, logging progress to Session["log"].
    /// </summary>
    private void StartRestore() {
        XmlDataDocument doc = Session["restoreFile"] as XmlDataDocument;
        int resApps = 0,
            resBundles = 0,
            resElements = 0,
            resResources = 0,
            resTemplates = 0,
            resTranslations = 0,
            resUsers = 0;
        try {
            Database db = new Database();
            db.OpenConnection();
            if(chkClearDatabase.Checked) {
                try {
                    db.DropTables(chkResApplications.Checked, chkResElements.Checked, chkResResources.Checked, chkResResources.Checked, chkResUsers.Checked, chkBakTemplates.Checked);
                } catch { }
                db.CreateTables(chkResApplications.Checked, chkResElements.Checked, chkResResources.Checked, chkResResources.Checked, chkResUsers.Checked, chkResTemplates.Checked);
            }
            if(chkResApplications.Checked) {
                LogText("Restoring applications...", false);
                using(Database.Command cmd = db.CreateCommand()) {
                    cmd.CommandText = "INSERT INTO " + db.TablePrefix + "applications (id, name, displayname, defaultlanguage, languages) VALUES (@id, @name, @displayname, @defaultlanguage, @languages)";
                    cmd.AddParameterWithValue("@id", string.Empty);
                    cmd.AddParameterWithValue("@name", string.Empty);
                    cmd.AddParameterWithValue("@displayname", string.Empty);
                    cmd.AddParameterWithValue("@defaultlanguage", string.Empty);
                    cmd.AddParameterWithValue("@languages", string.Empty);
                    XmlNodeList appsNodes = doc.GetElementsByTagName("applications")[0].ChildNodes;
                    foreach(XmlNode app in appsNodes) {
                        string id = string.Empty,
                               name = string.Empty,
                               displayname = string.Empty,
                               defaultlanguage = string.Empty,
                               languages = string.Empty;
                        foreach(XmlNode property in app) {
                            switch(property.Name.ToLower()) {
                                case "id":
                                    id = HttpUtility.HtmlDecode(property.InnerText);
                                    break;
                                case "name":
                                    name = Utils.HtmlDecode(property.InnerText);
                                    break;
                                case "displayname":
                                    displayname = Utils.HtmlDecode(property.InnerText);
                                    break;
                                case "defaultlanguage":
                                    defaultlanguage = HttpUtility.HtmlDecode(property.InnerText);
                                    break;
                                case "languages":
                                    languages = HttpUtility.HtmlDecode(property.InnerText.Trim());
                                    break;
                            }
                        }
                        if(string.IsNullOrEmpty(id) ||
                           string.IsNullOrEmpty(name) ||
                           string.IsNullOrEmpty(displayname) ||
                           string.IsNullOrEmpty(defaultlanguage) ||
                           string.IsNullOrEmpty(languages)) {
                            LogText("Invalid application " + displayname, true);
                        } else {
                            cmd.Parameters["@id"].Value = id;
                            cmd.Parameters["@name"].Value = name;
                            cmd.Parameters["@displayname"].Value = displayname;
                            cmd.Parameters["@defaultlanguage"].Value = defaultlanguage;
                            cmd.Parameters["@languages"].Value = languages;
                            try {
                                cmd.ExecuteNonQuery();
                                resApps++;
                            } catch(Exception ex) {
                                if(ex.Message.ToLowerInvariant().Contains("unique")) {
                                    LogText("Application already exists: " + name, true);
                                } else {
                                    LogText(ex.Message, true);
                                }
                            }
                        }
                    }
                }
                LogText("Restoring bundles...", false);
                using(Database.Command cmd = db.CreateCommand()) {
                    cmd.CommandText = "INSERT INTO " + db.TablePrefix + "bundles (id, name, application) VALUES (@id, @name, @application)";
                    cmd.AddParameterWithValue("@id", string.Empty);
                    cmd.AddParameterWithValue("@name", string.Empty);
                    cmd.AddParameterWithValue("@application", string.Empty);
                    XmlNodeList bundlesNodes = doc.GetElementsByTagName("bundles")[0].ChildNodes;
                    foreach(XmlNode bundle in bundlesNodes) {
                        string id = string.Empty,
                               name = string.Empty,
                               application = string.Empty;
                        foreach(XmlNode property in bundle) {
                            switch(property.Name.ToLower()) {
                                case "id":
                                    id = HttpUtility.HtmlDecode(property.InnerText);
                                    break;
                                case "name":
                                    name = Utils.HtmlDecode(property.InnerText);
                                    break;
                                case "application":
                                    application = Utils.HtmlDecode(property.InnerText);
                                    break;
                            }
                        }
                        if(string.IsNullOrEmpty(id) ||
                           string.IsNullOrEmpty(name) ||
                           string.IsNullOrEmpty(application)) {
                            LogText("Invalid bundle " + name, true);
                        } else {
                            cmd.Parameters["@id"].Value = id;
                            cmd.Parameters["@name"].Value = name;
                            cmd.Parameters["@application"].Value = application;
                            try {
                                cmd.ExecuteNonQuery();
                                resBundles++;
                            } catch(Exception ex) {
                                if(ex.Message.ToLowerInvariant().Contains("unique")) {
                                    LogText("Bundle already exists: " + name, true);
                                } else {
                                    LogText(ex.Message, true);
                                }
                            }
                        }
                    }
                }
            }
            if(chkResElements.Checked) {
                LogText("Restoring elements...", false);
                using(Database.Command cmd = db.CreateCommand()) {
                    cmd.CommandText = "INSERT INTO " + db.TablePrefix + "elements (applicationid, bundle, elementname, resourceid, comment) VALUES (@applicationid, @bundle, @elementname, @resourceid, @comment)";
                    cmd.AddParameterWithValue("@applicationid", string.Empty);
                    cmd.AddParameterWithValue("@bundle", string.Empty);
                    cmd.AddParameterWithValue("@elementname", string.Empty);
                    cmd.AddParameterWithValue("@resourceid", string.Empty);
                    cmd.AddParameterWithValue("@comment", string.Empty);
                    XmlNodeList elementsNodes = doc.GetElementsByTagName("element");
                    foreach(XmlNode element in elementsNodes) {
                        string applicationid = string.Empty,
                               bundle = string.Empty,
                               elementname = string.Empty,
                               resourceid = string.Empty,
                               comment = string.Empty;
                        foreach(XmlNode property in element) {
                            switch(property.Name.ToLower()) {
                                case "applicationid":
                                    applicationid = HttpUtility.HtmlDecode(property.InnerText);
                                    break;
                                case "bundle":
                                    bundle = Utils.HtmlDecode(property.InnerText);
                                    break;
                                case "elementname":
                                    elementname = Utils.HtmlDecode(property.InnerText);
                                    break;
                                case "resourceid":
                                case "translationid":
                                    resourceid = HttpUtility.HtmlDecode(property.InnerText);
                                    break;
                                case "comment":
                                    comment = Utils.HtmlDecode(property.InnerText);
                                    break;
                            }
                        }
                        if(string.IsNullOrEmpty(applicationid) ||
                           string.IsNullOrEmpty(elementname)) {
                            LogText("Invalid element " + elementname, true);
                        } else {
                            cmd.Parameters["@applicationid"].Value = applicationid;
                            cmd.Parameters["@bundle"].Value = bundle;
                            cmd.Parameters["@elementname"].Value = elementname;
                            cmd.Parameters["@resourceid"].Value = resourceid;
                            cmd.Parameters["@comment"].Value = comment;
                            try {
                                cmd.ExecuteNonQuery();
                                resElements++;
                            } catch(Exception ex) {
                                if(ex.Message.ToLowerInvariant().Contains("unique")) {
                                    LogText("Element already exists: " + elementname, true);
                                } else {
                                    LogText(ex.Message, true);
                                }
                            }
                        }
                    }
                }
            }
            if(chkResResources.Checked) {
                LogText("Restoring resources...", false);
                using(Database.Command cmdInsertResource = db.CreateCommand())
                using(Database.Command cmdInsertComment = db.CreateCommand()) {
                    cmdInsertResource.CommandText = "INSERT INTO " + db.TablePrefix + "resources (resourceid, language, translation, translatorcomment) VALUES (@id, @language, @translation, @comment)";
                    cmdInsertResource.AddParameterWithValue("@id", string.Empty);
                    cmdInsertResource.AddParameterWithValue("@language", string.Empty);
                    cmdInsertResource.AddParameterWithValue("@translation", string.Empty);
                    cmdInsertResource.AddParameterWithValue("@comment", string.Empty);
                    cmdInsertComment.CommandText = "INSERT INTO " + db.TablePrefix + "resourcecomments (resourceid, comment) VALUES (@id, @comment)";
                    cmdInsertComment.AddParameterWithValue("@id", string.Empty);
                    cmdInsertComment.AddParameterWithValue("@comment", string.Empty);
                    List<XmlNode> resourceNodes = new List<XmlNode>(); // xml format changed in > 1.3
                    foreach(XmlNode oldNode in doc.GetElementsByTagName("resource")) {
                        resourceNodes.Add(oldNode);
                    }
                    foreach(XmlNode oldNode in doc.GetElementsByTagName("translation")) {
                        resourceNodes.Add(oldNode);
                    }
                    int numNodes = resourceNodes.Count,
                        logIncrements = numNodes / 10;
                    foreach(XmlNode resource in resourceNodes) {
                        try {
                            string id = resource["id"].InnerText,
                                   comment = Utils.HtmlDecode(resource["comment"].InnerText),
                                   language = string.Empty;
                            XmlNodeList ndLanguages = resource.SelectNodes("languages/language");
                            if(string.IsNullOrEmpty(id) || ndLanguages.Count == 0) {
                                LogText("Invalid resource", true);
                                continue;
                            }
                            if(!string.IsNullOrEmpty(comment)) {
                                cmdInsertComment.Parameters["@id"].Value = id;
                                cmdInsertComment.Parameters["@comment"].Value = comment;
                                cmdInsertComment.ExecuteNonQuery();
                            }
                            cmdInsertResource.Parameters["@id"].Value = id;
                            foreach(XmlNode ndLanguage in ndLanguages) {
                                if(ndLanguage.Attributes.Count > 0) {
                                    cmdInsertResource.Parameters["@language"].Value = ndLanguage.Attributes[0].Value;
                                    cmdInsertResource.Parameters["@translation"].Value = Utils.HtmlDecode(ndLanguage.InnerText);
                                    if(ndLanguage.Attributes.Count > 1) {
                                        cmdInsertResource.Parameters["@comment"].Value = Utils.HtmlDecode(ndLanguage.Attributes[1].InnerText);
                                    } else {
                                        cmdInsertResource.Parameters["@comment"].Value = DBNull.Value;
                                    }
                                    try {
                                        cmdInsertResource.ExecuteNonQuery();
                                        resTranslations++;
                                    } catch(Exception ex) {
                                        LogText("Could not add resource for resource id " + id + ": " + ex.Message, true);
                                    }
                                }
                            }
                            resResources++;
                            if(resResources % logIncrements == 0) {
                                LogText(string.Format("Restored {0}/{1} resources", resResources, numNodes), false);
                            }
                        } catch(Exception ex) {
                            LogText("Invalid resource" + ex.Message, true);
                        }
                    }
                }
            }
            if(chkResTemplates.Checked) {
                LogText("Restoring templates...", false);
                using(Database.Command cmd = db.CreateCommand()) {
                    cmd.CommandText = "INSERT INTO " + db.TablePrefix + "templates (name, filename, filestart, fileend, line) VALUES (@name, @filename, @filestart, @fileend, @line)";
                    cmd.AddParameterWithValue("@name", string.Empty);
                    cmd.AddParameterWithValue("@filename", string.Empty);
                    cmd.AddParameterWithValue("@filestart", string.Empty);
                    cmd.AddParameterWithValue("@fileend", string.Empty);
                    cmd.AddParameterWithValue("@line", string.Empty);
                    XmlNodeList appsNodes = doc.GetElementsByTagName("template");
                    foreach(XmlNode app in appsNodes) {
                        string name = string.Empty,
                               filename = string.Empty,
                               filestart = string.Empty,
                               fileend = string.Empty,
                               line = string.Empty;
                        foreach(XmlNode property in app) {
                            switch(property.Name.ToLower()) {
                                case "name":
                                    name = Utils.HtmlDecode(property.InnerText);
                                    break;
                                case "filename":
                                    filename = Utils.HtmlDecode(property.InnerText);
                                    break;
                                case "filestart":
                                    filestart = Utils.HtmlDecode(property.InnerText);
                                    break;
                                case "fileend":
                                    fileend = Utils.HtmlDecode(property.InnerText);
                                    break;
                                case "line":
                                    line = Utils.HtmlDecode(property.InnerText);
                                    break;
                            }
                        }
                        if(string.IsNullOrEmpty(name) ||
                           string.IsNullOrEmpty(filename) ||
                           string.IsNullOrEmpty(line)) {
                            LogText("Invalid template " + name, true);
                        } else {
                            cmd.Parameters["@name"].Value = name;
                            cmd.Parameters["@filename"].Value = filename;
                            cmd.Parameters["@filestart"].Value = filestart;
                            cmd.Parameters["@fileend"].Value = fileend;
                            cmd.Parameters["@line"].Value = line;
                            try {
                                cmd.ExecuteNonQuery();
                                resTemplates++;
                            } catch(Exception ex) {
                                if(ex.Message.ToLowerInvariant().Contains("unique")) {
                                    LogText("Template already exists: " + name, true);
                                } else {
                                    LogText(ex.Message, true);
                                }
                            }
                        }
                    }
                }
            }
            if(chkResUsers.Checked) {
                LogText("Restoring users...", false);
                using(Database.Command cmd = db.CreateCommand()) {
                    cmd.CommandText = "INSERT INTO " + db.TablePrefix + "users (username, password, email, permissions, allowedprojects) VALUES (@username, @password, @email, @permissions, @allowedprojects)";
                    cmd.AddParameterWithValue("@username", string.Empty);
                    cmd.AddParameterWithValue("@password", Utils.SaltPassword("password"));
                    cmd.AddParameterWithValue("@email", string.Empty);
                    cmd.AddParameterWithValue("@permissions", string.Empty);
                    cmd.AddParameterWithValue("@allowedprojects", string.Empty);
                    XmlNodeList appsNodes = doc.GetElementsByTagName("user");
                    foreach(XmlNode app in appsNodes) {
                        string username = string.Empty,
                               email = string.Empty,
                               permissions = string.Empty,
                               allowedprojects = string.Empty;
                        foreach(XmlNode property in app) {
                            switch(property.Name.ToLower()) {
                                case "username":
                                    username = Utils.HtmlDecode(property.InnerText);
                                    break;
                                case "password":
                                    break;
                                case "email":
                                    email = Utils.HtmlDecode(property.InnerText);
                                    break;
                                case "permissions":
                                    permissions = HttpUtility.HtmlDecode(property.InnerText);
                                    break;
                                case "allowedprojects":
                                    allowedprojects = Utils.HtmlDecode(property.InnerText);
                                    break;
                            }
                        }
                        if(string.IsNullOrEmpty(username) ||
                           string.IsNullOrEmpty(permissions)) {
                            LogText("Invalid user " + username, true);
                        } else {
                            cmd.Parameters["@username"].Value = username;
                            cmd.Parameters["@email"].Value = email;
                            cmd.Parameters["@permissions"].Value = permissions;
                            cmd.Parameters["@allowedprojects"].Value = allowedprojects;
                            try {
                                cmd.ExecuteNonQuery();
                                resUsers++;
                            } catch(Exception ex) {
                                if(ex.Message.ToLowerInvariant().Contains("unique")) {
                                    LogText("User already exists: " + username, true);
                                } else {
                                    LogText(ex.Message, true);
                                }
                            }
                        }
                    }
                }
            }
            LogText(string.Format("Restored <strong>{0}</strong> applications, <strong>{1}</strong> bundles, <strong>{2}</strong> elements, <strong>{3}</strong> templates, <strong>{4}</strong> resources containing <strong>{5}</strong> translations and <strong>{6}</strong> users", resApps, resBundles, resElements, resTemplates, resResources, resTranslations, resUsers), false);
            LogText("Restore complete.", false);
            Session["restoreDone"] = bool.TrueString;
        } catch(Exception ex) {
            LogText("Error restoring #406: " + ex.Message, true);
            Session["restoreDone"] = bool.TrueString;
        }
        Thread worker = Session["worker"] as Thread;
        if(worker != null) {
            worker.Abort();
        }
    }

    /// <summary>
    /// Logs text to the progress BulletedList.
    /// </summary>
    /// <param name="text">The text to log.</param>
    /// <param name="error"><c>true</c> if the 'error' CSS class should be applied to the text.</param>
    private void LogText(string text, bool error) {
        List<ListItem> log = Session["log"] as List<ListItem>;
        ListItem itm = new ListItem(text);
        if(error) {
            itm.Attributes.Add("class", "error");
        }
        log.Add(itm);
    }

    /// <summary>
    /// Deserializes the restore log from the Session cache and re-populates the progress BulletedList.
    /// Detects completion of the operation and stops the timer.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
    protected void tmrRestore_Tick(object sender, EventArgs e) {
        try {
            List<ListItem> log = Session["log"] as List<ListItem>;
            lstProgress.Items.Clear();
            if(log == null) {
                log = new List<ListItem>();
            } else {
                lstProgress.Items.AddRange(log.ToArray());
            }
            if(bool.Parse(Session["restoreDone"].ToString())) {
                (sender as System.Web.UI.Timer).Enabled = false;
                Session.Remove("restoreFile");
                Session.Remove("restoreDone");
                imgProgress.Visible = false;
            }
        } catch {
            lstProgress.Items.Add("Error communicating with server, aborting.");
            Session.Remove("restoreFile");
            Session["restoreDone"] = bool.TrueString;
        }
    }
}
